package reliability.model;

import reliability.GraphParser;
import reliability.types.Settings;
import reliability.types.graph.*;

import javax.swing.event.EventListenerList;
import java.util.Map;
import java.util.NavigableMap;

public class GraphModel
{
    private Edges edges = new Edges();
    private Nodes nodes = new Nodes();
    private Settings settings = new Settings();

    private NetworkElement selectedItem;
    private EventListenerList eventListenerList = new EventListenerList();
    private VectorOfMinimalPaths minimalPaths = null;

    public GraphModel()
    {
    }

    public GraphModel(Nodes nodes, Edges edges, Settings settings)
    {
        init(nodes, edges, settings);
    }

    public void init()
    {
        init(new Nodes(), new Edges(), new Settings());
    }

    public void init(GraphModel graphModel)
    {
        init(graphModel.getNodes(), graphModel.getEdges(), graphModel.getSettings());
    }

    public void init(Nodes nodes, Edges edges, Settings settings)
    {
        this.nodes = nodes;
        this.edges = edges;
        this.settings = settings;
        minimalPaths = null;
        dispatchEvent(GraphModelEvent.GRAPH_INIT);
    }

    public VectorOfMinimalPaths getMinimalPaths()
    {
        if (minimalPaths == null)
        {
            GraphParser graphParser = new GraphParser(this);
            minimalPaths = graphParser.getMinimalPaths();
        }
        return minimalPaths;
    }

    public Settings getSettings()
    {
        return settings;
    }

    public double getDefaultNodeReliability()
    {
        return settings.defaultNodeReliability;
    }

    public void setDefaultNodeReliability(double reliability)
    {
        settings.defaultNodeReliability = reliability;
    }

    public double getDefaultEdgeReliability()
    {
        return settings.defaultEdgeReliability;
    }

    public void setDefaultEdgeReliability(double reliability)
    {
        settings.defaultEdgeReliability = reliability;
    }

    public int getNumberOfTests()
    {
        return settings.numberOfTests;
    }

    public void setNumberOfTests(int numberOfTests)
    {
        settings.numberOfTests = numberOfTests;
    }

    public Edges getEdges()
    {
        return edges;
    }

    public Nodes getNodes()
    {
        return nodes;
    }

    public NetworkElement getElement(Integer key)
    {
        NetworkElement element = null;
        if (nodes.containsKey(key))
        {
            element = nodes.get(key);
        }
        else if (edges.containsKey(key))
        {
            element = edges.get(key);
        }
        return element;
    }

    public NetworkElement getSelectedItem()
    {
        return selectedItem;
    }

    public Integer getSelectedItemKey()
    {
        Integer selectedItemKey = null;
        for (Map.Entry<Integer, Node> nodeEntry : nodes.entrySet())
        {
            if (nodeEntry.getValue().equals(selectedItem))
            {
                selectedItemKey = nodeEntry.getKey();
            }
        }
        for (Map.Entry<Integer, Edge> edgeEntry : edges.entrySet())
        {
            if (edgeEntry.getValue().equals(selectedItem))
            {
                selectedItemKey = edgeEntry.getKey();
            }
        }
        return selectedItemKey;
    }

    public void setSelectedItem(Integer key)
    {
        this.selectedItem = getElement(key);
        dispatchEvent(GraphModelEvent.SELECTION_CHANGED);
    }

    public Integer addNode(int row, int column)
    {
        Node node = new Node(settings.defaultNodeReliability, row, column);
        Integer newNodeKey = (!nodes.isEmpty()) ? nodes.lastKey() + 1 : 1;
        nodes.put(newNodeKey, node);
        updateEdgeKeys();
        dispatchEvent(GraphModelEvent.GRAPH_UPDATED);
        return newNodeKey;
    }

    public void addEdge(Integer key1, Integer key2)
    {
        if (!key1.equals(key2))
        {
            Edge edge = new Edge(settings.defaultEdgeReliability, key1, key2);
            int reservedLinesCount = countReservedLines(edge);
            if (reservedLinesCount < 2)
            {
                if (reservedLinesCount == 1)
                {
                    edge.setReservedLine(true);
                }
                Integer newKey = (!edges.isEmpty()) ? edges.lastKey() + 1 : nodes.lastKey() + 1;
                edges.put(newKey, edge);
                dispatchEvent(GraphModelEvent.GRAPH_UPDATED);
            }
        }
    }

    public int countReservedLines(Edge edgeToTest)
    {
        int counter = 0;
        for (Edge edge : edges.values())
        {
            if (!edge.equals(edgeToTest) && edge.hasSameEnds(edgeToTest))
            {
                counter++;
            }
        }
        return counter;
    }

    public void addEdgeFromSelectedNode(Integer secondNodeKey)
    {
        if (selectedItem instanceof Node)
        {
            Integer selectedNodeKey = getSelectedItemKey();
            addEdge(selectedNodeKey, secondNodeKey);
        }
    }

    public void deleteEdge(Integer edgeKey)
    {
        setSelectedItemNullIfItHasTheSameKey(edgeKey);
        edges.remove(edgeKey);
        updateEdgeKeys();
        dispatchEvent(GraphModelEvent.GRAPH_UPDATED);
    }

    public void deleteNode(Integer keyToDelete)
    {
        setSelectedItemNullIfItHasTheSameKey(keyToDelete);
        NavigableMap<Integer, Node> tailMap = nodes.tailMap(keyToDelete, false);
        Nodes headNodes = new Nodes(nodes.headMap(keyToDelete));

        removeEdgesContainingNode(keyToDelete);

        Integer updatedNodeKey = keyToDelete;
        for (Map.Entry<Integer, Node> nodeEntry : tailMap.entrySet())
        {
            Integer nodeKey = nodeEntry.getKey();
            replaceNodeKeysInEdges(nodeKey, updatedNodeKey);
            headNodes.put(updatedNodeKey, nodeEntry.getValue());
            updatedNodeKey++;
        }

        nodes = headNodes;
        updateEdgeKeys();
        dispatchEvent(GraphModelEvent.GRAPH_UPDATED);
    }

    private void setSelectedItemNullIfItHasTheSameKey(Integer key)
    {
        if (selectedItem == null)
            return;

        if (selectedItem.equals(getElement(key)))
        {
            selectedItem = null;
            dispatchEvent(GraphModelEvent.SELECTION_CHANGED);
        }
    }

    private void removeEdgesContainingNode(Integer nodeKey)
    {
        Edges updatedEdges = new Edges();
        for (Map.Entry<Integer, Edge> edgeEntry : edges.entrySet())
        {
            Edge edge = edgeEntry.getValue();
            if (!edge.getNode1Key().equals(nodeKey) && !edge.getNode2Key().equals(nodeKey))
            {
                updatedEdges.put(edgeEntry.getKey(), edge);
            }
        }
        edges = updatedEdges;
    }

    private void replaceNodeKeysInEdges(Integer from, Integer to)
    {
        for (Map.Entry<Integer, Edge> edgeEntry : edges.entrySet())
        {
            Edge edge = edgeEntry.getValue();
            if (edge.getNode1Key().equals(from))
            {
                edge.setNode1Key(to);
            }
            else if (edge.getNode2Key().equals(from))
            {
                edge.setNode2Key(to);
            }
        }
    }

    public void addListener(GraphModelListener listener)
    {
        eventListenerList.add(GraphModelListener.class, listener);
    }

    public void removeListener(GraphModelListener listener)
    {
        eventListenerList.remove(GraphModelListener.class, listener);
    }

    protected void dispatchEvent(int id)
    {
        minimalPaths = null;
        Object[] listeners = eventListenerList.getListenerList();
        GraphModelEvent graphModelEvent = new GraphModelEvent(this, id);
        for (int i = listeners.length - 2; i >= 0; i -= 2)
        {
            if (listeners[i] == GraphModelListener.class)
            {

                ((GraphModelListener) listeners[i+1]).graphModelChanged(graphModelEvent);
            }
        }
    }

    private void updateEdgeKeys()
    {
        if (edges.isEmpty())
        {
            return;
        }

        Integer edgeKey = nodes.lastKey();
        Edges updatedEdges = new Edges();
        for (Map.Entry<Integer, Edge> edgeEntry : edges.entrySet())
        {
            updatedEdges.put(++edgeKey, edgeEntry.getValue());
        }
        edges = updatedEdges;
    }

}
